在之前的文章中我們已經有深入探討序列化器的原理,而我們今天繼續深入探討序列化器本身
程式碼:https://github.com/class83108/drf_demo/tree/ad_serializers
今天重點如下:
我們先來看一下原本的輸出來看看我們如何透過自定義序列化器,來讓輸出更直觀
# GET http://127.0.0.1:8000/note/workspaces/
[
{
"id": 10,
"name": "DRF Project",
"owner": 1,
"members": [
1
],
"created_at": "2024-09-27T17:33:27.791189Z"
},
{
"id": 11,
"name": "Django Project",
"owner": 2,
"members": [
2
],
"created_at": "2024-09-27T17:33:27.796077Z"
}
]
首先StringRelatedField會將具備關聯性模型中的__str__
作為返回值,如果是多對多關係可以添加many=True
class WorkspaceSerializer(serializers.ModelSerializer):
owner = serializers.StringRelatedField(read_only=True)
members = serializers.StringRelatedField(many=True, read_only=True)
class Meta:
model = Workspace
fields = [
"id",
"name",
"owner",
"members",
"created_at",
]
read_only_fields = ["id", "created_at"]
[
{
"id": 10,
"name": "DRF Project",
"owner": "alice",
"members": [
"alice"
],
"created_at": "2024-09-27T17:33:27.791189Z"
},
{
"id": 11,
"name": "Django Project",
"owner": "bob",
"members": [
"bob"
],
"created_at": "2024-09-27T17:33:27.796077Z"
}
]
但是同時StringRelatedField
會將欄位變成只讀屬性,因此如果想要去修改該欄位變的不可行
這時候可以建立新的欄位然後使用source
指定資料來源
class WorkspaceSerializer(serializers.ModelSerializer):
# owner = serializers.StringRelatedField(read_only=True)
owner_name = serializers.CharField(source="owner.username", read_only=True)
# members = serializers.StringRelatedField(many=True, read_only=True)
members_name = serializers.StringRelatedField(
source="members", many=True, read_only=True
)
class Meta:
model = Workspace
fields = [
"id",
"name",
# "owner",
# "members",
"owner_name",
"members_name",
"created_at",
]
read_only_fields = ["id", "created_at"]
[
{
"id": 10,
"name": "DRF Project",
"owner_name": "alice",
"members_name": [
"alice"
],
"created_at": "2024-09-27T17:33:27.791189Z"
},
{
"id": 11,
"name": "Django Project",
"owner_name": "bob",
"members_name": [
"bob"
],
"created_at": "2024-09-27T17:33:27.796077Z"
}
]
我們完成剩下的需求
get_<field_name>
class WorkspaceSerializer(serializers.ModelSerializer):
# owner = serializers.StringRelatedField(read_only=True)
owner_name = serializers.CharField(source="owner.username", read_only=True)
# members = serializers.StringRelatedField(many=True, read_only=True)
members_name = serializers.StringRelatedField(
source="members", many=True, read_only=True
)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
document_count = serializers.SerializerMethodField()
class Meta:
model = Workspace
fields = [
"id",
"name",
# "owner",
# "members",
"owner_name",
"members_name",
"created_at",
"document_count",
]
read_only_fields = ["id", "created_at"]
def create(self, validated_data):
if isinstance(validated_data, list):
return Workspace.objects.bulk_create(validated_data)
return super().create(validated_data)
def get_document_count(self, instance):
return instance.documents.count()
def to_representation(self, instance):
representation = super().to_representation(instance)
representation["document_info"] = instance.documents.values("title")
return representation
[
{
"id": 10,
"name": "DRF Project",
"owner_name": "alice",
"members_name": [
"alice"
],
"created_at": "2024-09-27 17:33:27",
"document_count": 3,
"document_info": [
{
"title": "DRF Serializers"
},
{
"title": "DRF Views"
},
{
"title": "DRF Permissions"
}
]
},
{
"id": 11,
"name": "Django Project",
"owner_name": "bob",
"members_name": [
"bob"
],
"created_at": "2024-09-27 17:33:27",
"document_count": 3,
"document_info": [
{
"title": "Django form"
},
{
"title": "Django Views"
},
{
"title": "Django Admin"
}
]
}
]
這邊有一個地方還需要注意:
還記得我們昨天提到視圖類別中可以藉由self.get_queryset()
初始化queryset嗎?
我們現在的to_representation會造成N+1查詢問題,因此如果你確定要這個序列化器都必須返回這個欄位的話需要在調用時使用select_related
或 prefetch_related
來優化
workspaces = Workspace.objects.prefetch_related('documents')
既然序列化器也是一種類別來定義的,那是不是又可以回到Object-oriented programming(OOP)的特性:繼承,同時我們也可以利用在Django in 2024: 淺嚐Model, Url與Template學到的知識,使用指定某個欄位為物件的方式來達到序列化器的組合,讓人對於不同序列化器之間的關係有更明確的認知
因為Workspace與Document相同的欄位有created_at,因此我們可以將其做成一個基礎的序列化器,透過abstract = True
來宣告這只是一個抽象的序列化器,與之前Django in 2024: 淺嚐Model, Url與Template在Model中使用的方式相同
class BaseSerializer(serializers.ModelSerializer):
created_at = serializers.DateTimeField(read_only=True)
class Meta:
abstract = True
fields = "__all__"
class WorkspaceSerializer(BaseSerializer):
class Meta(BaseSerializer.Meta):
model = Workspace
fields = ["id", "name", "owner", "members", "created_at"]
class DocumentSerializer(BaseSerializer):
class Meta(BaseSerializer.Meta):
model = Document
fields = [
"id",
"title",
"content",
"workspace",
"created_by",
"created_at",
"updated_at",
]
這時候可能會看到class Meta(BaseSerializer.Meta),那這樣abstract = True不是也會繼承過去嗎?
但其實此時只會繼承到特定欄位例如fields
,例如我們改一下
class BaseSerializer(serializers.ModelSerializer):
created_at = serializers.DateTimeField(read_only=True)
class Meta:
abstract = True
fields = ["created_at"]
class WorkspaceSerializer(BaseSerializer):
class Meta(BaseSerializer.Meta):
model = Workspace
# fields = ["id", "name", "owner", "members", "created_at"]
可以看到輸出的欄位的確被繼承了
[
{
"created_at": "2024-09-27T17:33:27.791189Z"
},
{
"created_at": "2024-09-27T17:33:27.796077Z"
}
]
序列化器組合也就是在藉由多個序列化器組成一個序列化器,例如像Workspace的關係是由User模型作為owner的外鍵,而Document又有Workspace作為外鍵,這樣的關係,透過組合能夠更一目瞭然
class MemberSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ["id", "title"]
class WorkspaceSerializer(serializers.ModelSerializer):
owner = MemberSerializer(read_only=True)
members = MemberSerializer(many=True, read_only=True)
documents = DocumentSerializer(many=True, read_only=True)
class Meta:
model = Workspace
fields = ["id", "name", "owner", "members", "documents", "created_at"]
我們也能透過剛剛組合與繼承的特性,延伸出更多的序列化器的同時,不需要自己重寫程式碼
class BaseSerializer(serializers.ModelSerializer):
created_at = serializers.DateTimeField(read_only=True)
class Meta:
abstract = True
fields = "__all__"
class MemberSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
class BaseWorkspaceSerializer(BaseSerializer):
owner = MemberSerializer(read_only=True)
class Meta(BaseSerializer.Meta):
model = Workspace
fields = ["id", "name", "owner", "created_at"]
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ["id", "title"]
class WorkspaceListSerializer(BaseWorkspaceSerializer):
pass
class WorkspaceDetailSerializer(BaseWorkspaceSerializer):
members = MemberSerializer(many=True, read_only=True)
documents = DocumentSerializer(many=True, read_only=True)
class Meta(BaseWorkspaceSerializer.Meta):
fields = BaseWorkspaceSerializer.Meta.fields + ["members", "documents"]
來解釋一下其中的設計原理:
在昨天的文章中我們提到有哪些視圖類別能夠快速建立API端點,並且依據序列化器來返回對應的資料,在不同情境下我們可以更善用這些特性
關於資料的處理在Django REST framework: 序列化器與視圖函式 開啟API之旅已經有示範過這邊就不多提,這邊介紹在處理之前的驗證流程
Django REST framework(DRF)中的驗證流程如下
validate_<field_name>()
方法validate()
方法,最後返回驗證後的數據serializers.ValidationError
來報錯因此我們可以單獨針對特定欄位做驗證
class WorkspaceSerializer(serializers.ModelSerializer):
def validate_name(self, value):
if len(value) < 3:
raise serializers.ValidationError("名稱至少需要3個字以上。")
return value
class Meta:
model = Workspace
fields = ['id', 'name', 'owner', 'members']
或是直接在validate()
方法中定義不同欄位的資料驗證
class DocumentSerializer(serializers.ModelSerializer):
def validate(self, data):
if len(data['title']) > len(data['content']):
raise serializers.ValidationError("標題不能長於內容。")
return data
class Meta:
model = Document
fields = ['id', 'title', 'content', 'workspace', 'created_by']
在Django in 2024: Django Admin二次開發,打造屬於你的後台中,我們已經體驗到了context
(上下文)的美好與重要性,在DRF中我們也能在調用序列化器時,通過上下文的傳遞來返回特定資料
例如我們想要知道當前調用API的用戶,是不是擁有該Workspace
class WorkspaceDetailSerializer(BaseWorkspaceSerializer):
members = MemberSerializer(many=True, read_only=True)
documents = DocumentSerializer(many=True, read_only=True)
is_owner = serializers.SerializerMethodField()
def get_is_owner(self, instance):
request = self.context.get("request")
return request.user == instance.owner
class Meta(BaseWorkspaceSerializer.Meta):
fields = BaseWorkspaceSerializer.Meta.fields + [
"members",
"documents",
"is_owner",
]
我們來看一下調用視圖類別
class WorkspaceDetail(GenericAPIView):
queryset = Workspace.objects.all()
serializer_class = WorkspaceDetailSerializer
def get(self, request, pk):
workspace = self.get_object()
serializer = self.get_serializer(workspace)
return Response(serializer.data)
然後可以看到GET的輸出確實有新增欄位is_owner
{
"id": 11,
"name": "Django Project",
"owner": {
"id": 2,
"username": "bob",
"email": ""
},
"created_at": "2024-09-27T17:33:27.796077Z",
"members": [
{
"id": 2,
"username": "bob",
"email": ""
}
],
"documents": [
{
"id": 7,
"title": "Django form"
},
{
"id": 8,
"title": "Django Views"
},
{
"id": 9,
"title": "Django Admin"
}
],
"is_owner": false
}
這時候可能有疑問,奇怪我們剛剛在調用序列化器serializer = self.get_serializer(workspace)
也沒有傳遞上下文啊?
遇事不決看源始碼:
get_serializer
中調用self.get_serializer_context()
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs.setdefault('context', self.get_serializer_context())
return serializer_class(*args, **kwargs)
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
這時候可能會想,那如果我要傳遞上下文的關鍵字參數沒有在預設的get_serializer_context裡面能不能這樣寫
serializer = self.get_serializer(workspace, context={"request": request})
答案是可以,但是不建議,因為這樣同時我們覆蓋掉了其他預設值,所以正確做法應該如下
context = self.get_serializer_context()
context.update({"some_extra_data": "value"})
serializer = self.get_serializer(workspace, context=context)
今天我們又更深入探討序列化器的領域